Blog about App development for iOS

Properties, properties, properties!!!

Swift lets you initialize properties in multiple ways. This article quickly goes through all different permutations var/let/static allowed.

Let's start with a very simple struct. This struct declares all kinds of properties. Each one executes a print statement and returns a constant with a different string each.

struct PropertyTest {
	var prop1: String {
		print("- computing1...")
		return "prop1" + #function
	}
	
	var prop2: String = {
		print("- computing2...")
		return "prop2" + #function
	}()
	
	lazy var prop3: String = {
		print("- computing3...")
		return "prop3" + #function
	}()
	
// ----> !!! 'let' declarations cannot be computed properties
//	let prop4: String {
//		print("computing...")
//		return #function
//	}
	
	let prop5: String = {
		print("- computing5...")
		return "prop5" + #function
	}()
	
// ----> !!! 'lazy' cannot be used on a let
//	lazy let prop6: String = {
//		print("computing...")
//		return #function
//	}()
	
	static var prop7: String {
		print("- computing7...")
		return "prop7" + #function
	}
	
	static var prop8: String = {
		print("- computing8...")
		return "prop8" + #function
	}()
	
// ----> !!! 'lazy' must not be used on an already-lazy global
//	static lazy var prop9: String = {
//		print("computing...")
//		return #function
//	}()
	
// ----> !!! 'let' declarations cannot be computed properties
//	static let prop10: String {
//		print("computing...")
//		return #function
//	}
	
	static let prop11: String = {
		print("- computing11...")
		return "prop11" + #function
	}()
	
// ----> !!! 'lazy' cannot be used on a let
//	static lazy let prop12: String = {
//		print("computing...")
//		return #function
//	}()
}

errors, errors!

Some methods are commented with a compile error code. The most interesting one is the one that states that lazy must not be used on an already-lazy global. This means that statics are lazy by default. We will talk about it when we look into prop8

Lets initialize our struct

var test = PropertyTest()

> - computing2...
> - computing5...

Just by instantiating PropertyTest, we are greeted by the initialization of prop2 and prop5.

prop2 and prop5 are pre-set whenever we build a new distance of PropertyTest. They are stored properties. Non-lazy stored properties are set on initialization regardless of whether we are dealing with a struct or a class. More about stored properties later.

prop1

var prop1: String {
	print("- computing1...")
	return "prop1" + #function
}
print(test.prop1)
> - computing1...
> prop1prop1

print(test.prop1)
> - computing1...
> prop1prop1

That's it, every time we access prop1, we compute it. It is a computed property and the function name we are calling is prop1

Computed properties cannot be changed. Therefore the test.prop1 = "1" assignment does not compile.

prop2

var prop2: String = {
	print("- computing2...")
	return "prop2" + #function
}()
print(test.prop2)
> prop2PropertyTest

print(test.prop2)
> prop2PropertyTest

That's it, every time we access prop2, it is already computed. It is a stored property and the function name is PropertyTest because it was built in its constructor.

Stored properties can be changed. test.prop2 = "1" is perfectly legal.

prop3

lazy var prop3: String = {
	print("- computing3...")
	return "prop3" + #function
}()
print(test.prop3)
> - computing3...
> prop3prop3

print(test.prop3)
> prop3prop3

The first time we access prop3, it gets computed. Any successive calls will not compute it and return the same value. It is like a lazy stored property

Lazy properties can be changed, therefore test.prop3 = "3" is valid.

prop5

let prop5: String = {
	print("- computing5...")
	return "prop5" + #function
}()
var test = PropertyTest()

print(test.prop5)
> prop5PropertyTest

print(test.prop5)
> prop5PropertyTest

This one is exactly like prop2. It is a stored property which is built in the constructor.

prop7

static var prop7: String {
	print("- computing7...")
	return "prop7" + #function
}
print(PropertyTest.prop7)
> - computing7...
> prop7prop7

print(PropertyTest.prop7)
> - computing7...
> prop7prop7

prop7 is a static computed property, like with prop1, its value is computed every time we access it. Also, like with prop1, it is not possible to set its value. Computed properties are get-only properties.

prop8

static var prop8: String = {
	print("- computing8...")
	return "prop8" + #function
}()
print(PropertyTest.prop8)
> - computing8...
> prop8PropertyTest

print(PropertyTest.prop8)
> prop8PropertyTest

In this case, we are dealing with a static stored property. Because it is static, it is lazy by default, meaning that the first time we access it will be computed and successive calls will just reuse the computed value.

Assigning a value to prop8 is perfectly ok. PropertyTest.prop8 = "8" compiles without any problem.

prop11

static let prop11: String = {
  print("- computing11...")
  return "prop11" + #function
}()
print(PropertyTest.prop11)
> - computing11...
> prop11PropertyTest

print(PropertyTest.prop11)
> prop11PropertyTest

prop11, like prop8, is an immutable stored property

lazy vs struct

Something interesting happens when we declare test as a const

let test = PropertyTest()

If we want to access our lazy property, prop3, the code will not compile

print(test.prop3)  ->  !!! Cannot use mutating getter on immutable value: 'test' is a 'let' constant

What is this madness? Why does it not compile anymore?

The answer could not be easier: lazy stored properties are mutating and test is now a constant. Since we are dealing with a struct here, which is a value type, if we declare it as a constant, we cannot access any lazy property it might have declared, as it would mean we are changing test.

But there is more...

var test = PropertyTest()
var test1 = test

print(test1.prop3)
> computing3...
> prop3prop3

print(test.prop3)
> computing3...
> prop3prop3


var test2 = test
print(test2)
> prop3prop3

test1 is a copy of test before calling prop3 therefore pop3 is initialized both for test1 and test...

...but test2 is a copy of test after prop3 has been lazily initialized, and when accessing it, it is using a cached value from test.

initializing

We have seen that we can change the value of stored properties. What happens when we do that on prop8 which is static and lazy?

PropertyTest.prop8 = "8"
> - computing8...

print(test.prop8)
> 8

By accessing prop8 we first initialize it, and then we set it. Therefore one can see that it is first being set with its default value, and then the value changes to the one we want it to have.

Final thoughts

No matter what the nature of the topic we dive in, there will always be underlying complexities and small differences in behavior... Even for something as trivial such as stored and computed properties!



Published on September 1, 2020

Tagged with: